#!/usr/bin/python3

print("[INFO] Initializing")

import socket
import codecs
import sqlite3
import os


## defining big strings (like sql queries) and other constants

SQL_QUERY_CREATE_TABLE = """
CREATE TABLE entries (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(255) NOT NULL,
    url VARCHAR(4096) NOT NULL,
    approved BOOLEAN NOT NULL CHECK (approved IN (0, 1)),
    sourceip VARCHAR(4096) NOT NULL
);
"""

END_OF_HTTP_REQUEST = "\r\n\r\n".encode("UTF-8")



## define Request and Responce classes to simplify the actual code

class Request:
    def __init__(self, request_data, decode_headers):
        try:
            decoded_request = request_data.decode("UTF-8")
            self.successful = True

        except Exception as e:
            print("[ERROR] Failed to decode request from {} ({})".format(addr[0] + ":" + str(addr[1]), e))
            self.successful = False

        if self.successful:
            self.method = decoded_request.split(" ")[0]
            self.path = decoded_request.split(" ")[1].split("?")[0]
            self.args = {}

            if len( decoded_request.split(" ")[1].split("?") ) > 1: # don't parse args of none present
                args = decoded_request.split(" ")[1].split("?")[1].split("&")
                if args[0] != "":
                    for current_arg in args:
                        arg = current_arg.split("=")
                        print(arg)
                        self.args[arg[0]] = arg[1]

            if decode_headers:
                self.headers = {}

                raw_headers = decoded_request.split("\r\n\r\n")[0].split("\r\n")[1:]

                for raw_header in raw_headers:
                    header = raw_header.split(": ")
                    self.headers[header[0]] = header[1]


class Responce:
    def __init__(self):
        self.code = ""
        self.content_type = ""
        self.body = ""

    def generate(self):
        responce_data = "HTTP/1.1 {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}".format(self.code, self.content_type, len(self.body.encode("UTF-8")), self.body)
        return responce_data.encode("UTF-8")


## defining intify() function to make code reading easier
## and to create a failsafe wrapper around the int() built-in function

def intify(x):
    try:
        return int(x)
    except Exception as e:
        print("Failed to parse integer from {} ({})".format(x, e))
        return False

## defining readfile() function to make code easier to read

def readfile(path):
    try:
        return codecs.open(path, "r", encoding = "UTF-8").read()

    except FileNotFoundError:
        print("No such file: {}".format(path))
        return False

    except Exception as e:
        print("Failed to read file {} ({})".format(path, e))
        return False

## defining make_entry() function to make code more readable

def make_entry(name, url):
    return readfile("www/search-block.html").format(name, url, url) 


## making sure all the required directories are present

required_directories = ["files", "www"]

for directory in required_directories:
    try:
        os.listdir(directory)

    except FileNotFoundError:
        print("[NOTICE] Creating {}/ folder".format(directory))
        os.mkdir(directory)


## connecting to the database (and creating new one if it's not there already)

db_connection = sqlite3.connect("files/instances.db")
db_cursor = db_connection.cursor()

db_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='entries';")
if len(db_cursor.fetchall()) == 0:
    db_cursor.execute(SQL_QUERY_CREATE_TABLE)


## creating socket and starting server

print("[INFO] Server starting...")

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind( ("", 8080) )
s.listen()

print("[INFO] Server started")


## dealing with incoming connections

while True:
    #try:
        conn, addr = s.accept()

        print("[LOG] Accepted connection from {}".format(addr[0] + ":" + str(addr[1])))

        ## allowing very high timeouts as they
        ## are possible on slow networks or during
        ## high loads (like 2G or dial-up connection)
        conn.settimeout(1800)
        
        connection_alive = True
        request_buffer = bytes()
        post_request_data = bytes()

        ## receiving request
        
        while True:
            new_data = conn.recv(1)

            if len(new_data) == 0:
                ## connection closed, cleaning up
                conn.close()
                connection_alive = False
                break

            request_buffer += new_data

            if request_buffer[-4:] == END_OF_HTTP_REQUEST:
                if request_buffer[:4] == "POST".encode("UTF-8"):
                    temp_req = Request(request_buffer, True)

                    print(temp_req.headers)

                    content_length = int(temp_req.headers["Content-Length"])

                    for i in range(content_length):
                        print("qwe")
                        post_request_data += conn.recv(1)

                ## request probably finished here, stopping the loop
                break

        ## check if client is still there

        if not connection_alive:
            print("[LOG] Lost connection from {}".format(addr[0] + ":" + str(addr[1])))
            conn.close()
            continue

        ## processing the request

        req = Request(request_buffer, False)

        ## if it is not a proper request, ignore it
        if not req.successful:
            conn.close()
            continue

        ## logging requests to terminal

        print("[LOG] {} - {} {}".format( addr[0] + ":" + str(addr[1]), req.method, req.path ))

        if req.method == "GET":
            ## giving index page at /
            if req.path == "/":
                responce_body = readfile("www/index.html")

                if responce_body == False:
                    conn.close()
                    continue

                res = Responce()
                res.code = "200"
                res.content_type = "text/html"
                res.body = responce_body

                responce_data = res.generate()
                conn.send(responce_data)

                conn.close()

            ## notify about the wrong method to help if someone is reverse-engineering an open-source app (I would do that too, so anyway)
            elif req.path == "/search":
                db_cursor.execute("SELECT name, url FROM entries WHERE approved = 1;")
                results = db_cursor.fetchall()

                res = Responce()
                res.code = "200"
                res.content_type = "text/html"

                page_begin = readfile("www/search-begin.html")

                if not page_begin:
                    conn.close()
                    continue

                res.body += page_begin

                #for result in results:
                #    if res.args[]

                page_end = readfile("www/search-end.html")

                if not page_end:
                    conn.close()
                    continue

                res.body += page_end

                responce_data = res.generate()
                conn.send(responce_data)

                conn.close()

            ## showing all approved entries at /all
            elif req.path == "/all":
                db_cursor.execute("SELECT name, url FROM entries WHERE approved = 1;")
                results = db_cursor.fetchall()

                res = Responce()
                res.code = "200"
                res.content_type = "text/html"

                page_begin = readfile("www/search-begin.html")

                if not page_begin:
                    conn.close()
                    continue

                res.body += page_begin

                for result in results:
                    res.body += make_entry(result[0], result[1])

                page_end = readfile("www/search-end.html")

                if not page_end:
                    conn.close()
                    continue

                res.body += page_end

                responce_data = res.generate()
                conn.send(responce_data)

                conn.close()

            ## at everything else return 404 Not Found error
            else:
                res = Responce()
                res.code = "404"
                res.content_type = "text/plain"
                res.body = "{} is not a supported request path".format(req.path)

                responce_data = res.generate()
                conn.send(responce_data)

                conn.close()
                

        ## HEAD is not supported yet to reduce debug time and possible error surface
        elif req.method == "HEAD":
            res = Responce()
            res.code = "501"
            res.content_type = "text/plain"
            res.body = "{} is not a supported request method - yes, it IS intentional (and yes, it WILL be added in upcoming releases; it's not there yet to make a working release faster by reducing the amount of code to debug and polish)".format(req.method)

            responce_data = res.generate()
            conn.send(responce_data)

            conn.close()

        ## other methods are not supported yet
        else:
            res = Responce()
            res.code = "501"
            res.content_type = "text/plain"
            res.body = "{} is not a supported request method".format(req.method)

            responce_data = res.generate()
            conn.send(responce_data)

            conn.close()

    ## handling Ctrl+C signal
    #except KeyboardInterrupt:
    #    print("\r[INFO] Exiting properly")
    #    break

    ## show thread exception as non-critical error
    #except Exception as e:
    #    print("[ERROR] Got exception {}".format(e))
    #    conn.close()
    #    continue
